# -*-coding:utf-8-*-

import logging
import pymongo
import json
import motor
from pymongo.collection import ReturnDocument
from utils.misc import generate_random_no, extract_fields
from tornado.gen import coroutine, Return
from bson.objectid import ObjectId

Base_Document_Structure = {
    'field': 'v0',

    'object_field': {
        'sub_object_field': 'sv0',
    },

    'array_field': [
        {
            'sub_object_field': {
                # 这其中的字段可以直接通过 mongo 更新(待确认)
            },
            'sub_array_field': [
                # 二级以上数组中的字段只能靠人工逻辑更新，mongo 并不提供原生支持(待确认)
            ]
        },
    ]
}


class BaseRecord(object):
    def __init__(self, data):
        self.data = data

    def __getattr__(self, item):
        return self._get_optional_field(item)

    def _get_optional_field(self, *args):
        field = self.data
        for arg in args:
            if arg in field:
                field = field[arg]
            else:
                return None
        return field

    @property
    def id_str(self):
        return str(self.data['_id'])


class SubRecordTemp(BaseRecord):
    def __getattr__(self, item):
        if 'special_item' == item:
            args = ('spec', 'item')
        else:
            args = (item,)
        return self._get_optional_field(*args)


'''
- 功能 -
更新文档指定字段
更新内嵌文档中指定字段
更新数组字段中指定对象中的字段
'''


class BaseDao(object):
    def __init__(self, db, collection_name):
        self.logger = logging.getLogger()
        self.collection = db.get_collection(collection_name)

    def _get_record_object(self, data):
        return BaseRecord(data)

    @coroutine
    def get_record_by_id(self, rid, rid_field_name='_id'):
        rid_field_value = ObjectId(rid) if '_id' == rid_field_name else rid
        filter_cond = {rid_field_name: rid_field_value}
        data = yield self.find_one(filter_cond)
        if data is not None:
            result = self._get_record_object(data)
            raise Return(result)
        else:
            raise Return(None)

    @staticmethod
    def extract_fields(src, required_fields=None, optional_fields=None):
        return extract_fields(src, required_fields, optional_fields)

    @coroutine
    def insert_one_with_unique_no(self, fields, field_base_name, length):
        field_no = '%s_no' % field_base_name
        field_id = '%s_id' % field_base_name
        while True:
            try:
                fields[field_no] = str(generate_random_no(length))
                r = yield self.insert_one(fields)
                result = {field_id: str(r.inserted_id), field_no: fields[field_no]}
            except Exception as e:
                if '_id' in fields:
                    del fields['_id']
            else:
                raise Return(result)

    @coroutine
    def get_paging_result(self, filter_cond, projection, id_field_name, on_per_item, **orig_kwargs):
        if 'tags' in orig_kwargs:
            tags = orig_kwargs['tags']
            tags = tags.split(',')
            filter_cond.update(tags={'$all': tags})
        kwargs = self.extract_fields(orig_kwargs, optional_fields=['skip', 'limit', 'sort'])
        if 'skip' in kwargs:
            kwargs['skip'] = int(kwargs['skip'])
        if 'limit' in kwargs:
            kwargs['limit'] = int(kwargs['limit'])
        kwargs['sort'] = [('_id', pymongo.DESCENDING)]
        result = []
        cursor = self.collection.find(filter_cond, projection, **kwargs)
        while (yield cursor.fetch_next):
            r = cursor.next_object()
            record_id = str(r.pop('_id'))
            if id_field_name is not None:
                r[id_field_name] = record_id
            append = True
            if on_per_item is not None:
                append = on_per_item(r, **orig_kwargs)
            if append:
                result.append(r)
        raise Return(result)

    @coroutine
    def count(self, *args, **kwargs):
        result = yield self.collection.count(*args, **kwargs)
        raise Return(result)

    def find(self, *args, **kwargs):
        return self.collection.find(*args, **kwargs)

    # @coroutine
    # def find(self, *args, **kwargs):
    #     result = []
    #     cursor = self.collection.find(*args, **kwargs)
    #     while (yield cursor.fetch_next):
    #         result.append(cursor.next_object())
    #     raise Return(result)

    def aggregate(self, pipeline, **kwargs):
        return self.collection.aggregate(pipeline, **kwargs)

    @coroutine
    def find_one(self, filter=None, *args, **kwargs):
        result = yield self.collection.find_one(filter, *args, **kwargs)
        raise Return(result)

    @coroutine
    def insert_one(self, document, bypass_document_validation=False):
        result = yield self.collection.insert_one(document, bypass_document_validation=bypass_document_validation)
        raise Return(result)

    @coroutine
    def insert_many(self, documents, ordered=True,
                    bypass_document_validation=False):
        result = yield self.collection.insert_many(documents, ordered=ordered,
                                                   bypass_document_validation=bypass_document_validation)
        raise Return(result)

    @coroutine
    def update_one(self, filter, update, upsert=False,
                   bypass_document_validation=False,
                   collation=None):
        result = yield self.collection.update_one(filter, update, upsert=upsert,
                                                  bypass_document_validation=bypass_document_validation,
                                                  collation=collation)
        raise Return(result)

    @coroutine
    def update_many(self, filter, update, upsert=False,
                    bypass_document_validation=False, collation=None):
        result = yield self.collection.update_many(filter, update, upsert=upsert,
                                                   bypass_document_validation=bypass_document_validation,
                                                   collation=collation)
        raise Return(result)

    @coroutine
    def delete_one(self, filter, collation=None):
        result = yield self.collection.delete_one(filter, collation=collation)
        raise Return(result)

    @coroutine
    def delete_many(self, filter, collation=None):
        result = yield self.collection.delete_many(filter, collation=collation)
        raise Return(result)

    @coroutine
    def find_one_and_update(self, filter, update,
                            projection=None, sort=None, upsert=False,
                            return_document=ReturnDocument.BEFORE, **kwargs):
        result = yield self.collection.find_one_and_update(filter, update, projection=projection, sort=sort,
                                                           upsert=upsert, return_document=return_document, **kwargs)
        raise Return(result)

    @coroutine
    def find_one_and_delete(self, filter,
                            projection=None, sort=None, **kwargs):
        result = yield self.collection.find_one_and_delete(filter, projection=projection, sort=sort, **kwargs)
        raise Return(result)

    '''
        sub-object
    '''

    @coroutine
    def get_object_field(self, filter_cond, field):
        projection = {field: 1, '_id': 0}
        result = yield self.find_one(filter_cond, projection)
        if result and field in result:
            raise Return(result[field])
        else:
            raise Return(None)

    @coroutine
    def delete_object_field(self, filter_cond, field):
        result = yield self.update_one(filter_cond, {'$unset': {field: 0}})
        raise Return(1 == result.matched_count)

    '''
        sub-object-field
    '''

    @coroutine
    def get_sub_object_field(self, filter_cond, field, sub_field):
        sub_field_name = '%s.%s' % (field, sub_field)
        projection = {sub_field_name: 1, '_id': 0}
        new_filter_cond = dict({sub_field_name: {'$exists': 1}}, **filter_cond)
        result = yield self.find_one(new_filter_cond, projection)
        if result is not None:
            raise Return(result[field][sub_field])
        else:
            raise Return(None)

    @coroutine
    def add_sub_object_field(self):
        pass

    @coroutine
    def update_sub_object_field(self, filter_cond, field, sub_field, data):
        sub_field_name = '%s.%s' % (field, sub_field)
        result = yield self.update_one(filter_cond, {'$set': {sub_field_name: data}})
        raise Return(1 == result.matched_count)

    @coroutine
    def delete_sub_object_field(self, filter_cond, field, sub_field):
        sub_field_name = '%s.%s' % (field, sub_field)
        result = yield self.update_one(filter_cond, {'$unset': {sub_field_name: 0}})
        raise Return(1 == result.matched_count)

    '''
       sub-array-element
    '''

    @coroutine
    def get_sub_array_element(self, filter_cond, field):
        result = yield self.find_one(filter_cond, {'%s.$' % field: 1})
        raise Return(result[field][0])

    @coroutine
    def add_sub_array_element(self, filter_cond, field, data):
        result = yield self.update_one(filter_cond, {'$push': {field: data}})
        raise Return(1 == result.matched_count)

    # NOTE: `filter_cond` must include query conditions to locate the element of array
    @coroutine
    def update_sub_array_element(self, filter_cond, field, data):
        current = yield self.get_sub_array_element(filter_cond, field)
        current.update(data)
        result = yield self.update_one(filter_cond, {'$set': {'%s.$' % field: current}})
        raise Return(1 == result.matched_count)

    @coroutine
    def delete_sub_array_element(self, filter_cond, field, data):
        result = yield self.update_one(filter_cond, {'$pull': {field: data}})
        raise Return(1 == result.matched_count)
